<?php
/**
 * SimpleDatexM.php
 * 
 * This file contains the SimpleDatexM class, a simple object representation of table rows.
 * @author Robert Schmidt <robert@wildpack.de>
 * @version 1.0
 * @package wildpack
 * @subpackage dataaccess.wpdatex
 * @dependencies: wildpack.base.observe.*
 */


/**
 * An ORM implementation for multiple data rows
 * 
 * This is a simple object representation of table or view rows. The class is declared as abstract. Methods containing 
 * data about the table staructur must be implemented in extending classes. The SimpleDatexM class does only support tables 
 * with a single primary key field. The primary key is used to identify datasets. The class does neither exclusively 
 * supports the auto_increment feature, nor prevents using it. Values, created by the database are not exclusively 
 * reported back to the SimpleDatexM objects. You should reload the object after saving when using this functionality.
 * 
 * How to extend this class:
 * <code>
 * class MyData extends SimpleDatexM
 * {
 * 		//Implement the abstract getTable method
 * 		public function getTable()
 * 		{
 * 			return 'MyTable';
 * 		}
 * 
 * 		//Implement the abstract getFieldType method
 * 		public function getFieldType($fieldname)
 * 		{
 * 			switch ($fieldname) {
 * 				case 'intfield' : return 'number';
 * 				case 'floatfield' : return 'number';
 * 				case 'textfield' : return 'string';
 * 			}
 * 		}
 * 
 * 		//Implement the abstract getFields method
 * 		public function getFields()
 * 		{
 * 			return array(
 * 				'intfield',
 * 				'floatfield',
 * 				'textfield'
 * 			);
 * 		}
 * 		
 * 		//Implement the abstract getPkField method
 * 		public function getPkField()
 * 		{
 * 			return 'intfield';
 * 		}
 * 
 * 		//Implement the abstract isView method, views are not writeable
 * 		public function isView()
 * 		{
 * 			return false;
 * 		}
 * }
 * </code>
 * 
 * How to use the extended class:
 * <code>
 * $con = new WPConnectMySQL(array(
 * 		'location' => 'localhost/mydb',
 * 		'user' => 'me',
 * 		'password' => 'my_password'
 * ));
 * 
 * //Adding new datasets
 * $md = new MyData($con);
 * $md->add();
 * $md->set('intfield', 1);
 * $md->set('floatfield', 1.1);
 * $md->set('textfield', 'Lorem ipsum');
 * $md->save();
 * 
 * //Fill fields with equal content the easy way
 * $md->add();
 * $md->set('intfield', 2);
 * $md->add();
 * $md->set('intfield', 3);
 * $md->set('textfield', 'a value, all datasets should have', true); //Sets the field textfield for all datasets in object
 * $md->set('floatfield', 1.1, true);
 * 
 * //Load dataset
 * $md = new MyData($con);
 * $md = $md->load(array(
 * 		'floatfield' => 1.1
 * ));
 * 
 * //Iterate through
 * $md->rewind();
 * while($md->findNext()) {
 * 		print($md->get('textfield'));
 * }
 * 
 * //Update specific datasets (where floatfield is 1.2)
 * $md->rewind();
 * while($md->findNext('floatfield', 1.2)) {
 * 		$md->set('floatfield', 1.1);
 * }
 * $md->save();
 * 
 * //Remove all datasets
 * $md->remove();
 * 
 * </code>
 * 
 * @author Robert Schmidt <robert@wildpack.de>
 * @version 1.0
 * @package wildpack
 * @subpackage dataaccess.wpdatex
 * @dependencies: wildpack.base.observe.*
 * @abstract
 */
abstract class SimpleDatexM extends WpObservable
{
	/**
	 * @var string The error space to use for error logging
	 */
	private static $_espc = '22';
	
	/**
	 * @var array The list of condition modifiers
	 */
	private $ConditionMods = array('!!', '<<', '>>', '<=', '>=', '[]', '()', '%%');
	
	/**
	 * @var The connector to use accessing the datasource
	 */
	public $Connector = null;
	
	/**
	 * @var The data as associtative array with fieldnames as keys
	 */
	public $Data = array();
	
	/**
	 * @var The index of the currently active dataset
	 */
	public $Index = -1;
	
	/**
	 * @var The log object used to log error messages to
	 */
	public $Log = null;
	
	/**
	 * @var bool True if data has been loaded, false else
	 */
	public $Loaded = false;
	
	/**
	 * This method retrieves the value which is used by an select statement to specify the fields to return.
	 * Basically the return value should be * but there are cases where fields aliases are used
	 */
	protected function _getSelectFields()
	{
		return '*';
	}
	
	/**
	 * The class constructor
	 * @param object $connector [optional] The connector to be used for datasource connection
	 */
	public function __construct($connector, $log)
	{
		$this->Connector = $connector;
		$this->Log = $log;
		
		$this->addEvents(array(
			'add', 'postremove', 'preremove', 'postsave', 'presave', 'postload', 'preload'
		));
	}
	
	/**
	 * Adds a new dataset to the list and sets the index to this dataset. You may also fill the fields usind the 
	 * data parameter though it is an optional parameter.
	 * @param array $data An associative array of fields and values for the new dataset 
	 */
	public function add($data = null)
	{
		$loc = basename(__FILE__).'/'.__METHOD__;
		
		if ($this->isView() === true) {
			$this->Log->logError($loc, '', 'View is not editable', 'It is not possible to add datasets to a view.', self::$_espc.'008');
			return;
		}
		
		if (is_null($data) || !is_array($data)) {
			$this->Data[] = array();
		} else {
			$this->Data[] = $data;
		}
		
		$this->Index = count($this->Data)-1;
		$this->Data[$this->Index]['_new'] = true;
		$this->fireEvent('add', array($this, $data, $this->Index));
	}
	
	/**
	 * Counts data in datasource
	 * @param array $condition An associative array used for condition. Basically this array should contain 
	 * fieldnames (keys) and values to build up the search query. However, to enhance the functionality the
	 * load function searches for field prefixes in order to do other conditions than only equality. These prefixes 
	 * are: !! = NOT, %% = LIKE, [] = IN, << = less than, >> bigger than, <= less or equal, >= bigger or equal.
	 * 
	 * Example:
	 * <code>
	 * 
	 * </code>
	 *
	 * @return int The number of datasets matching the criteria
	 */
	public function count($condition=null)
	{
		$loc = basename(__FILE__).'/'.__METHOD__;
		
		if (is_null($this->Connector)) {
			$this->Log->logError($loc, '', 'No connector', 'No connector was sepcified.', self::$_espc.'000');
			return -1;
		}
		if (is_null($condition)) {
			$condition = array();
		}
		
		$c = '';
		foreach ($condition as $field => $value) {
			$mod = '';
			if (in_array(substr($field,0,2), $this->ConditionMods)) {
				$mod = substr($field,0,2);
				$field = substr($field,2);
			}
			switch($mod) {
				case '[]' : $c .= $field.' IN (\''.implode('\',\'',$value).'\')'; break;
				case '%%' : $c .= $field.' LIKE \''.$value.'\''; break;
				default : 
					if ($this->getFieldType($field) === 'string') {
						$value = '\''.$value.'\'';
					}
					$c .= $mod === '!!' ? 'NOT ' : '';
					$c .= $field;
					switch($mod) {
						case '<<' : $c .= ' < '; break;
						case '>>' : $c .= ' > '; break;
						case '<=' : $c .= ' <= '; break;
						case '>=' : $c .= ' >= '; break;
						default :  $c .= ' = '; break;
					}
					$c .= $value.' AND ';
			}
		}
		
		if ($c !== '') {
			$c = 'WHERE '.substr($c, 0, strlen($c)-5);
		}
			
		if (!$this->Connector->select('count('.$this->getPkField().')', $this->getTable(), $c)) {
			$this->Log->logError($loc, '', 'Error in method', 'The method WPConnect->select did not finish successfully.', self::$_espc.'001');
			return -1;
		}		
		$result = $this->Connector->getResult('arrays');
		
		//-- Check result --
		if (is_null($result) || !is_array($result)) {
			$this->Log->logError($loc, '', 'Invalid result', 'The connector returned an invalid result. Array expected.', self::$_espc.'002');
			return -1;
		}
		
		return $result[0][0];
	}
	
	/**
	 * A simple filter function used to filter the loaded datasets
	 * @param string $fieldname The fieldname of the field used for filtering
	 * @param mixed $value The value to use for filtering
	 * @param string $method [optional] The method used for filtering (equal, not equal)- Default: equal
	 */
	public function filter($fieldname, $value, $method='equal')
	{
		$newArray = array();
		
		foreach ($this->Data as $row) {
			switch ($method) {
				case 'equal' : 
					if ($row[$fieldname] === $value) {
						$newArray[] = $row;
					} break;
				case 'not equal' :
					if ($row[$fieldname] !== $value) {
						$newArray[] = $row;
					} break;
			}
		}
		
		$this->Data = $newArray;
	}
	
	/**
	 * Based on the dataset currently index, finds the next dataset matching the criteria and sets the index to 
	 * this dataset.
	 * If no criteria specified the function sets the index to the next dataset in loaded list or returns false if the 
	 * currently index dataset is the last.
	 * 
	 * <code>
	 * 
	 * //Iterate throught all your data and prints the content of the field myfield
	 * $dt = new MyDatexMExt($connector);
	 * if ($dt->load()) {
	 * 		$dt->rewind();
	 * 		while ($dt->findNext) {
	 * 			print($dt->get('myfield');
	 * 		}
	 * }
	 * 
	 * //Iterate thorugh all data where myfield does not contain 'a_value' and prints the fieldvalue
	 * $dt->rewind();
	 * while ($dt->findNext('myfield', 'a_value', 'not equal') {
	 * 		print($dt->get('myfield');
	 * }
	 * 
	 * </code>
	 * 
	 * @param string $fieldname [optional] The fieldname of the field used for lookup
	 * @param mixed $value [optional] The value to use for lookup
	 * @param string $method [optional] The method used for filtering ('equal', 'not equal')- Default: equal
	 * @return bool True if the index points to a valid dataset, false else
	 */
	public function findNext($fieldname='', $value=null, $method='equal')
	{
		$this->Index++;

		if ($this->Index > count($this->Data)-1) {
			$this->Index = -1;
			return false;
		}
		
		for ($this->Index; $this->Index<count($this->Data); $this->Index++)
		{
			if ($fieldname === '') {
				return true;
			} else {
			  switch ($method) {
					case 'equal' : 
						if ($this->Data[$this->Index][$fieldname] === $value) {
							return true;
						} break;
					case 'not equal' :
						if ($this->Data[$this->Index][$fieldname] !== $value) {
							return true;
						} break;
				}
			}
		}
		
		$this->Index = -1;
		return false;
	}
	
	/**
	 * Retrieves the value of the specified fieldname of the currently indexed dataset
	 * @param string $fieldname The fieldname to retrieve the value of
	 * @return mixed The value of the field
	 */
	public function get($fieldname)
	{
		if ($this->Index > -1 && array_key_exists($this->Index, $this->Data)) {
			if (array_key_exists($fieldname, $this->Data[$this->Index])) {
				return $this->Data[$this->Index][$fieldname];
			}
		}
		return null;
	}
	
	/**
	 * Retrieves the name of the table to use
	 * @abstract
	 * @return 
	 */
	public abstract function getTable();
	
	/**
	 * Retrieves an array of fields of the table
	 * @return array An array of fields
	 */
	public abstract function getFields();
	
	/**
	 * Retrieves the type of the field specified by name
	 * @param string $fieldname The name of the field to retrieve the type of
	 * @return string The type of the field. One of the following: string, number
	 */
	public abstract function getFieldType($fieldname);
	
	/**
	 * Retrieves the name of the field which is used as primary key
	 * @return string The primary key field
	 */
	public abstract function getPkField();

	/**
	 * Determines wether the used table is a view. Views are readable only.
	 * @return bool True if used table is a view, false else.
	 */
	public abstract function isView();
	
	/**
	 * Retrieves the number of datasets currently loaded 
	 * @return int The number of datasets currently loaded
	 */
	public function length()
	{
		if (is_array($this->Data)) {
			return count($this->Data);
		}
		
		return 0;
	}
	
	/**
	 * Loads data from datasource
	 * @param array $condition An associative array used for condition. Basically this array should contain 
	 * fieldnames (keys) and values to build up the search query. However, to enhance the functionality the
	 * load function searches for field prefixes in order to do other conditions than only equality. These prefixes 
	 * are: !! = NOT, %% = LIKE, [] = IN,() = IN, << = less than, >> bigger than, <= less or equal, >= bigger or equal.
	 * 
	 * Examples:
	 * <code>
	 * 
	 * //Simple lookup by equality
	 * $o->load(array(
	 * 	'field1' => 'value_to_match'
	 * ));
	 * 
	 * //Lookup using unequality
	 * $o->load(array(
	 * 	'!!field2' => 'value_not_to_match'
	 * ));
	 * 
	 * //Lookup witch matching one of a list
	 * $o->load(array(
	 * 	'[]field3' => array('option1', 'option2', 'option3')
	 * ));
	 * 
	 * 
	 * //Same as above but enhanced for using in webpages. Imagine the url: index.php?cmd=loadfromdb&()field1=(1,2,3)
	 * $field = '()field1';
	 * $value = $_GET[$field]; //Will get string (1,2,3)
	 * $o->load(array(
	 * 	$field => $value
	 * ));
	 * //The example above will lead to the sql query SELECT * FROM yourtable WHERE field1 IN (1,2,3)
	 * 
	 * </code>
	 * 
	 * 
	 * @param string $sort [optional] The fields to sort by
	 * @param string $dir [optional] The sorting direction (ASC or DESC)
	 * @param int $limit [optional] The maximum number of datasets to load
	 * @param int $start [optional] The dataset to start with, when limiting
	 * @return int The number of loaded datasets 
	 */
	public function load($condition=array(), $sort='', $dir='ASC', $limit=0, $start=0)
	{
		$loc = basename(__FILE__).'/'.__METHOD__;
		$this->reset();
		
		if (is_null($this->Connector)) {
			$this->Log->logError($loc, '', 'No connector', 'No connector was sepcified.', self::$_espc.'000');
			return -1;
		}
		
		$c = '';
		foreach ($condition as $field => $value) {
			$mod = '';
			if (in_array(substr($field,0,2), $this->ConditionMods)) {
				$mod = substr($field,0,2);
				$field = substr($field,2);
			}
			switch($mod) {
				case '[]' :
					if (is_array($value)) {
						$c .= $field.' IN (\''.implode('\',\'',$value).'\') AND '; break;
					} else {
						$c .= $field.' IN '.$value.' AND '; break;
					}
				case '()' :
					if (is_array($value)) {
						$c .= $field.' IN (\''.implode('\',\'',$value).'\') AND '; break;
					} else {
						$c .= $field.' IN '.$value.' AND '; break;
					}
				case '%%' : $c .= $field.' LIKE \''.$value.'\' AND '; break;
				default : 
					if ($this->getFieldType($field) === 'string') {
						$value = '\''.$value.'\'';
					}
					$c .= $mod === '!!' ? 'NOT ' : '';
					$c .= $field;
					switch($mod) {
						case '<<' : $c .= ' < '; break;
						case '>>' : $c .= ' > '; break;
						case '<=' : $c .= ' <= '; break;
						case '>=' : $c .= ' >= '; break;
						default :  $c .= ' = '; break;
					}
					$c .= $value.' AND ';
			}
		}
		if ($c !== '') {
			$c = 'WHERE '.substr($c, 0, strlen($c)-5);
		}
		
		$this->fireEvent('preload', array($this));
		if (!$this->Connector->select($this->_getSelectFields(), $this->getTable(), $c, $limit, $start, $sort, $dir)) {
			$this->Log->logError($loc, '', 'Error in method', 'The method WPConnect->select did not finish successfully.', self::$_espc.'001');
			return -1;
		}		
		$result = $this->Connector->getResult('assocs');
		
		//-- Check result --
		if (is_null($result) || !is_array($result)) {
			$this->Log->logError($loc, '', 'Invalid result', 'The connector returned an invalid result. Array expected.', self::$_espc.'002');
			return -1;
		}
		
		$this->Data = $result;
		$this->Loaded = true;
		$this->fireEvent('postload', array($this, count($this->Data)));
		return count($this->Data);
	}
	
	/**
	 * Remove all datasets
	 * @return int The number of removed datasets
	 */
	public function remove()
	{
		$loc = basename(__FILE__).'/'.__METHOD__;
		$blnCheck = false;
		
		if ($this->isView()) {
			$this->Log->logError($loc, '', 'View is not editable', 'It is not possible to add datasets to a view.', self::$_espc.'008');
			return false;
		}
		if (is_null($this->Connector)) {
			$this->Log->logError($loc, '', 'No connector', 'No connector was sepcified.', self::$_espc.'000');
			return -1;
		}
		
		if ($this->fireEvent('preremove', array($this)) === false) {
			return false;
		}
		$pkf = $this->getPkField();
		$isNum = $this->getFieldType($pkf) === 'number';
		$this->Connector->startMultiRequest(false);
		
		foreach ($this->Data as $row) {
			if ($isNum) {
				$c = 'WHERE '.$pkf.' = '.strval($row[$pkf]);
			} else {
				$c = 'WHERE '.$pkf.' = \''.$row[$pkf].'\'';
			}
			
			if (!array_key_exists('_new', $row)) {
				$blnCheck = $this->Connector->delete($this->getTable(), $c);
			}
			
			if ($blnCheck === false) {
				return false;
			}
		}
		
		$ret = $this->Connector->stopMultiRequest(true);
		if ($ret === true) {
			$this->fireEvent('postsave', array($this));
		}
		return $ret;
	}
	
	/**
	 * Resets the object to represents an empty object. The connector information will be kept.
	 */
	public function reset()
	{
		$this->Data = array();
		$this->Index = -1;
		$this->Loaded = false;
	}
	
	/**
	 * Sets the internal data pointer to the position before the first dataset.
	 * @see findNext
	 */
	public function rewind()
	{
		$this->Index = -1;
	}
	
	/**
	 * Save all loaded datasets
	 * @return bool True if successfully saved, false else
	 */
	public function save()
	{
		$loc = basename(__FILE__).'/'.__METHOD__;
		if ($this->isView()) {
			$this->Log->logError($loc, '', 'View is not editable', 'It is not possible to add datasets to a view.', self::$_espc.'008');
			return false;
		}
		if (is_null($this->Connector)) {
			$this->Log->logError($loc, '', 'No connector', 'No connector was sepcified.', self::$_espc.'000');
			return false;
		}
		if ($this->fireEvent('presave', array($this)) === false) {		
			return false;
		}
		
		$blnCheck = false;
		$pkf = $this->getPkField();
		$isNum = $this->getFieldType($pkf) === 'number';
		$stopMR = false;
		if ($this->Connector->isMultiRequest() !== true) {
			$this->Connector->startMultiRequest(true, true, false);
			$stopMR = true;
		}
		$fds = $this->getFields();
		$cleanedData = array();
		
		foreach ($this->Data as $row) {
			$new = false;
			$upd = false;
			$c = '';
			
			if (array_key_exists($pkf, $row)) {
				if ($isNum) {
					$c = 'WHERE '.$pkf.' = '.strval($row[$pkf]);
				} else {
					$c = 'WHERE '.$pkf.' = \''.$row[$pkf].'\'';
				}
			}
			
			if (array_key_exists('_new', $row)) {
				$new = true;
			}
			if (array_key_exists('_update', $row)) {
				$upd = true;
			}
			
			$cleanedRow = array();
			foreach ($fds as $fd) {
				if (array_key_exists($fd, $row)) {
					$cleanedRow[$fd] = $row[$fd];
				}
			}
			$cleanedData[] = $cleanedRow;
			
			if ($new === true) {
				$blnCheck = $this->Connector->insert($this->getTable(), $cleanedRow);
			} else {
				if ($upd === true) {
					if ($c === '') {
						return false;
					}
					$blnCheck = $this->Connector->update($this->getTable(), $cleanedRow, $c);
				}
			}
			
			if ($blnCheck === false) {
				return false;
			}
		}
		if ($stopMR === true) {
			$ret = $this->Connector->stopMultiRequest(true);
		} else {
			$ret = count($this->Data);
		}
		if ($ret > -1) {
			$this->Data = $cleanedData;
			$this->fireEvent('postsave', array($this));
		} else {
			$this->Log->logError($loc, '', 'Error in method', 'Method WPConnect->stopMultiRequest did not finish successfully.', self::$_espc.'009');
			return false;
		}
		return true;
	}
	
	/**
	 * Set the value of a field
	 * @param string $fieldname The name of the field to set
	 * @param mixed $value The value to set the field to
	 * @param boolean $all [optional] True to set the fields in all datasets, false to set only the indexed dataset 
	 */
	public function set($fieldname, $value, $all=false)
	{
		if ($all === false) {
			if ($this->Index > -1 && array_key_exists($this->Index, $this->Data)) {
				$this->Data[$this->Index][$fieldname] = $value;
				$this->Data[$this->Index]['_update'] = true;
			}
			return;
		}
		
		for ($i=0; $i<count($this->Data); $i++) {
			$this->Data[$i][$fieldname] = $value;
			$this->Data[$i]['_update'] = true;
		}
	}
	
	/**
	 * Retrieve either the currently indexed dataset or all datasets as an object. If a classname is 
	 * specified, the object will be created by instantiation of the class. 
	 * The class or the object should be of a subclass of SimpleDatexS, but must not. This method will 
	 * instantiate the class (if classname given) and copy the connector to the $Connector property, store the dataset into the 
	 * $Data property and sets the _isNew property to the object.
	 * @param mixed $target Either the name of the class to instantiate or the object to store the data to or null to create object of stdClass
	 * @param bool $all [optional] True to retrieve all datasets (if an object was provided, it will be cloned), 
	 * false to retrieve only the index dataset
	 * @return mixed Either the class instance as object or an array of instances if multiple datasets were 
	 * retrieved.
	 */
	public function to($target, $all=false)
	{
		if ($all === true) {
			if (count($this->Data) < 1) {
				return array();
			}
		} else {
			if ($this->Index === -1 || !array_key_exists($this->Index, $this->Data)) {
				return null;
			}
		}
		
		$af = $this->getFields();
		$ad = $all === false ? array($this->Data[$this->Index]) : $this->Data;
		$ar = array();
		$o = null;
		foreach ($ad as $de) {
			unset($o);
			if (is_null($target)) {
				$o = new stdClass();
			} else {
				if (is_string($target)) {
					$o = new $target();
				}
				else {
					$o = clone $target;
				}
				$o->Connector = clone $this->Connector;
				$o->_isNew = array_key_exists('_new', $de);
				$o->Data = array();
			}
			
			//-- Copy fields (skip system markers) --
			foreach ($af as $f) {
				if (is_null($target)) {
					$o->$f = $de[$f];
				} else {
					$o->Data[$f] = $de[$f];
				}
			}
			
			$ar[] = $o;
		}
		
		return $all === true ? $ar : $ar[0];
	}
}

/* End of file SimpleDatexM.php */
/* Location: ./wildpack/dataaccess/wpdatex/SimpleDatexM.php */ 